package com.redxiii.safesync;

import java.io.File;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.FileConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.redxiii.safesync.container.Container;
import com.redxiii.safesync.container.LocalContainer;
import com.redxiii.safesync.container.RemoteContainer;
import com.redxiii.util.cli.CmdLineIO;


public abstract class SyncHandler {

	private static final Logger logger = LoggerFactory.getLogger(SyncHandler.class); 
	
	private File fLocal;
	private File fRemote;
	private String password;
	private CryptoTool cryptoTool;
	
	protected FileConfiguration config;
	protected boolean automatic = false;
	
	protected abstract void getConfiguration();
	
	protected boolean checkConfiguration() {
		
		try {
			fLocal = new File(config.getString("local"));
			fRemote = new File(config.getString("remote"));
			password = config.getString("password");

			if (!fLocal.exists() || !fLocal.isDirectory()) {
				logger.warn("Invalid local folder: {}", fLocal.getAbsolutePath());
				return false;
			}
			
			if (!fRemote.exists() || !fRemote.isDirectory()) {
				logger.warn("Invalid remote folder: {}", fRemote.getAbsolutePath());
				return false;
			}
			
			try {
				cryptoTool = new CryptoToolWithGZip(password);
				RemoteContainer.verifyContainer(fRemote, cryptoTool);
			} catch(Exception e) {
				logger.error("Invalid password. \"{}\" ",e.getMessage());
				return false;
			}
			
			return true;
		} catch(Exception e) {
			logger.error("Error checking configuration",e);
		}
		return false;
	}
	protected boolean sync() {
		do {
			if (automatic && checkConfiguration())
				break;
				
			getConfiguration();
		} while (!checkConfiguration());
		
		LocalContainer localContainer = new LocalContainer();
		RemoteContainer remoteContainer = new RemoteContainer();
		
		boolean keepAlive = true;
		
		CmdLineIO io = new CmdLineIO();
		io.setTimeout(1000 * 30 * 1);
		
		Long localSyncTime;
		Long remoteSyncTime;
		do {
			localContainer.init(fLocal, cryptoTool);
			remoteContainer.init(fRemote, cryptoTool);
			
			remoteSyncTime = remoteContainer.getSyncDate();
			localSyncTime = config.getLong("lastSync", 0L);
			
			Map<String,Archive> remoteArchives = remoteContainer.getArchiveMap();
			Map<String,Archive> localArchives = localContainer.getArchiveMap();
			
			
			Set<Archive> localChanges = new HashSet<Archive>();
			Set<Archive> remoteChanges = new HashSet<Archive>();
			Set<Archive> localDeleted = new HashSet<Archive>();
			Set<Archive> remoteDeleted = new HashSet<Archive>();
			{
				
				for (Archive local : localArchives.values()) {
					Archive remote = remoteArchives.get(local.getAbsoluteName());
					if (remote == null) {
						if (local.getModifiedDate() > remoteSyncTime)
							localChanges.add(local);
						else
							remoteDeleted.add(local);
						
					} else if (local.isNewerThen(remote)){
						localChanges.add(local);
					}
				}
			}
			{
				for (Archive remote : remoteArchives.values()) {
					Archive local = localArchives.get(remote.getAbsoluteName());
					if (local == null) {
						if (remote.getModifiedDate() > localSyncTime)
							remoteChanges.add(remote);
						else
							localDeleted.add(remote);
					} else if (remote.isNewerThen(local)) {
						remoteChanges.add(remote);
					}
				}
			}
			
			if (localChanges.size() > 0) {
				logger.debug("Updating file(s) on remote folder");
				transfer(localChanges, remoteContainer);
				remoteContainer.updateContainerDetail();
			}
			if (remoteChanges.size() > 0) {
				logger.debug("Updating file(s) on local folder");				
				transfer(remoteChanges, localContainer);
			}
			if (remoteDeleted.size() > 0) {
				logger.debug("Removing file(s) on remote folder") ;
			}

			config.setProperty("lastSync", System.currentTimeMillis());
			try {
				config.save();
			} catch (ConfigurationException e) {
				logger.error("Error saving configuration",e);
			}
			
			keepAlive = io.getBoolean("Continue (s/n)", Boolean.TRUE);
			
		} while (keepAlive && automatic);
		
		return true;
	}
	
	private void transfer(Set<Archive> archives, Container container) {
		
		logger.debug("{} files to update", archives.size());
		long totalSize = 0;
		long start = System.currentTimeMillis();
		
		for (Archive archive : archives) {
			logger.debug("Updating {}", archive.getAbsoluteName());
			long fileStart = System.currentTimeMillis();
			container.update(archive);
			
			double seconds = ((double)(System.currentTimeMillis() - fileStart)) / 1000D;
			double rate = archive.getSize() / seconds;
			
			logger.debug("Updated in {}s. Rate: {} /s", seconds, humanReadableByteCount((long)rate, true));
			totalSize += archive.getSize();
		}
		if (totalSize > 0) {
			double seconds = ((double)(System.currentTimeMillis() - start)) / 1000D;
			double rate = totalSize / seconds;
			logger.debug("Transfer done in {}s. Rate: {} /s", seconds, humanReadableByteCount((long)rate, true));	
		}
		logger.debug("Done");
	}
	
	public static String humanReadableByteCount(long bytes, boolean si) {
	    int unit = si ? 1000 : 1024;
	    if (bytes < unit) return bytes + " B";
	    int exp = (int) (Math.log(bytes) / Math.log(unit));
	    String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp-1) + (si ? "" : "i");
	    return String.format("%.1f %sB", bytes / Math.pow(unit, exp), pre);
	}
}
